package com.wlyuan.index.domain.source.factory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.wlyuan.core.domain.event.DomainEventPublisher;
import com.wlyuan.core.domain.valueobj.*;
import com.wlyuan.core.exception.AssertUtils;
import com.wlyuan.core.exception.ValueError;
import com.wlyuan.index.api.dto.QuotationIndexDTO;
import com.wlyuan.index.api.dto.combine.CombineIndexDTO;
import com.wlyuan.index.api.dto.order.OrderIndexDTO;
import com.wlyuan.index.api.dto.waybill.BalanceIndexDTO;
import com.wlyuan.index.api.dto.waybill.BillIndexDTO;
import com.wlyuan.index.api.dto.waybill.WaybillIndexDTO;
import com.wlyuan.index.domain.index.event.CombineQuoteUpdated;
import com.wlyuan.index.domain.source.Combine;
import com.wlyuan.index.domain.source.Source;
import com.wlyuan.index.domain.source.Waybill;
import com.wlyuan.index.domain.source.assembler.*;
import com.wlyuan.index.domain.source.repository.SourceRepository;
import com.wlyuan.index.domain.source.utils.SourceUtils;
import com.wlyuan.index.domain.source.valueobj.Modifiers;
import com.wlyuan.index.domain.source.valueobj.SourceId;
import com.wlyuan.index.infrastructure.config.SourceProperties;
import com.wlyuan.index.remote.facade.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.var;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Service
@Slf4j
@RequiredArgsConstructor
public class SourceFactory {
    private final SourceRepository sourceRepository;
    private final SourceProperties sourceProperties;
    private final OrderFacade orderFacade;
    private final WaybillFacade waybillFacade;
    private final CombineFacade combineFacade;
    private final QuotationFacade quotationFacade;
    private final BillFacade billFacade;

    private boolean expired(LocalDateTime time) {
        if (null == time) {
            return true;
        }

        // 7天之前的数据刷新索引需要重新加载全部数据
        return time.plusDays(sourceProperties.getExpireDays()).isBefore(LocalDateTime.now());
    }

    private void addBounds(WaybillId waybillId) {
        // 支付信息
        var balances = billFacade.listBalances(waybillId);
        if (CollectionUtils.isNotEmpty(balances)) {
            balances.forEach(this::create);
        }

        // 报价信息
        var quotations = quotationFacade.listQuotations(waybillId);
        if (CollectionUtils.isNotEmpty(quotations)) {
            quotations.forEach(this::create);
        }

        // 账单信息
        var bills = billFacade.listBills(waybillId);
        if (CollectionUtils.isNotEmpty(bills)) {
            bills.forEach(this::create);
        }
    }

    private void addBounds(CombineId combineId) {
        var quotations = quotationFacade.listQuotations(combineId);
        if (CollectionUtils.isNotEmpty(quotations)) {
            quotations.forEach(this::create);
        }
    }

    private void addBounds(BillIndexDTO bill) {
        var boundId = new SourceId(new BillId(bill.getBillId()));
        bill.getWaybillIds().forEach(waybillId -> {
            var sourceId = new SourceId(new WaybillId(waybillId));
            sourceRepository.bound(sourceId, boundId);
        });
    }

    private void addBounds(QuotationIndexDTO quotation) {
        var quotationId = new QuotationId(quotation.getQuotationId());
        if (SourceUtils.isCombineQuote(quotation)) {
            var sourceId = (new SourceId(new CombineId(quotation.getSourceId())));
            sourceRepository.bound(sourceId, new SourceId(quotationId));
            // 发布合单报价更新事件
            DomainEventPublisher.publish(new CombineQuoteUpdated(quotation));
        } else {
            var sourceId = (new SourceId(new WaybillId(quotation.getSourceId())));
            sourceRepository.bound(sourceId, new SourceId(quotationId));
        }
    }

    private void addBounds(BalanceIndexDTO balance) {
        var boundId = new SourceId(Modifiers.PAYMENT, new WaybillId(balance.getWaybillId()));
        var sourceId = new SourceId(new WaybillId(balance.getWaybillId()));
        sourceRepository.bound(sourceId, boundId);
    }

    private void addBounds(CombineIndexDTO combine) {
        var boundId = new SourceId(new CombineId(combine.getCombineId()));
        combine.getWaybills().forEach(waybill -> {
            var sourceId = (new SourceId(new WaybillId(waybill.getWaybillId())));
            sourceRepository.bound(sourceId, boundId);
        });
    }

    public Source create(OrderId orderId) {
        var order = orderFacade.getOrderById(orderId);
        AssertUtils.assertNotNull(order, new ValueError("订单不存在"), orderId);
        this.create(order);
        return sourceRepository.ofId(new SourceId(orderId));
    }

    public Source create(WaybillId waybillId) {
        var waybill = waybillFacade.getWaybillById(waybillId);
        AssertUtils.assertNotNull(waybill, new ValueError("运单不存在"), waybillId);
        this.create(waybill);
        this.addBounds(waybillId);
        return sourceRepository.ofId(new SourceId(waybillId));
    }

    public Source create(CombineId combineId) {
        var combine = combineFacade.getCombineById(combineId);
        AssertUtils.assertNotNull(combine, new ValueError("合单不存在"), combineId);
        this.create(combine);
        this.addBounds(combineId);
        return sourceRepository.ofId(new SourceId(combineId));
    }

    public void create(OrderIndexDTO order) {
        var source = new Source(order);
        sourceRepository.save(source);
    }

    public void create(WaybillIndexDTO waybill) {
        var source = new Source(waybill);
        sourceRepository.save(source);
    }

    public void create(CombineIndexDTO combine) {
        var source = new Source(combine);
        sourceRepository.save(source);
        this.addBounds(combine);
    }

    public void create(BillIndexDTO bill) {
        var source = new Source(bill);
        sourceRepository.save(source);
        this.addBounds(bill);
    }

    public void create(QuotationIndexDTO quotation) {
        var source = new Source(quotation);
        sourceRepository.save(source);
        this.addBounds(quotation);
    }

    public void create(BalanceIndexDTO balance) {
        var source = new Source(balance);
        sourceRepository.save(source);
        this.addBounds(balance);
    }


    private List<Source> getSourcesById(SourceId sourceId) {
        var sources = new ArrayList<Source>();
        var source = sourceRepository.ofId(sourceId);
        if (null == source) {
            return sources;
        }

        sources.add(source);
        sources.addAll(sourceRepository.list(source.getBoundIds()));
        return sources;
    }

    public JSONObject getSourceById(OrderId orderId) {
        var source = new JSONObject();
        source.put("order", orderFacade.getOrderById(orderId));
        return source;
    }

    public JSONObject getSourceById(WaybillId waybillId) {
        var source = new JSONObject();
        var sources = this.getSourcesById(new SourceId(waybillId));
        var waybill = waybillFacade.getWaybillById(waybillId);
        var quotations = quotationFacade.listQuotations(waybillId);
        var bills = billFacade.listBills(waybillId);
        var balances = billFacade.listBalances(waybillId);
        source.put("sources", JSON.toJSON(sources));
        source.put("waybill", JSON.toJSON(waybill));
        source.put("quotations", JSON.toJSON(quotations));
        source.put("bills", JSON.toJSON(bills));
        source.put("balances", JSON.toJSON(balances));
        return source;
    }

    public JSONObject getSourceById(CombineId combineId) {
        var source = new JSONObject();
        var sources = this.getSourcesById(new SourceId(combineId));
        var combine = combineFacade.getCombineById(combineId);
        var quotations = quotationFacade.listQuotations(combineId);
        source.put("sources", JSON.toJSON(sources));
        source.put("combine", JSON.toJSON(combine));
        source.put("quotations", JSON.toJSON(quotations));
        return source;
    }

    private Map<SourceId, Source> getSourceMap(Collection<SourceId> sourceIds) {
        var sourceMap = sourceRepository.getSourceMap(sourceIds);
        sourceIds.forEach(sourceId -> {
            // FIXME: 视业务情况调整源数据过期是否需要重新加载
            if (sourceMap.containsKey(sourceId)) {
                return;
            }
            logger.warn("Source is missing: {}", sourceId);
            if (Modifiers.WAYBILL == sourceId.getModifier()) {
                sourceMap.put(sourceId, this.create((WaybillId) sourceId.decode()));
            } else if (Modifiers.COMBINE == sourceId.getModifier()) {
                sourceMap.put(sourceId, this.create((CombineId) sourceId.decode()));
            }
        });
        return sourceMap;
    }

    public List<Waybill> getWaybillByIds(List<WaybillId> waybillIds) {
        if (CollectionUtils.isEmpty(waybillIds)) {
            return Collections.emptyList();
        }
        var sourceIds = waybillIds.parallelStream().map(SourceId::new).collect(Collectors.toList());
        var sourceMap = getSourceMap(sourceIds);
        var boundsMap = sourceRepository.getBoundsMap(sourceIds);
        var waybills = new ArrayList<Waybill>();
        sourceMap.forEach((sourceId, source) -> {
            var waybill = WaybillAssembler.assemble(source.decode());
            var bounds = boundsMap.get(sourceId);
            if (CollectionUtils.isNotEmpty(bounds)) {
                bounds.forEach(bound -> {
                    if (bound.isBalance()) {
                        waybill.addBalances(BalanceAssembler.assemble(bound.decode()));
                    } else if (bound.isQuotation()) {
                        waybill.addQuotations(QuotationAssembler.assemble(bound.decode()));
                    } else if (bound.isBill()) {
                        waybill.addBills(BillAssembler.assemble(bound.decode()));
                    }
                });
            }
            waybills.add(waybill);
        });
        return waybills;
    }

    public Waybill getWaybillById(WaybillId waybillId) {
        var source = sourceRepository.ofId(new SourceId(waybillId));
        if (null == source) {
            logger.info("加载运单索引数据: {}", waybillId);
            source = this.create(waybillId);
        }
        var waybill = WaybillAssembler.assemble(source.decode());
        if (null == waybill) {
            logger.error("Illegal waybill source: {}", source);
            return null;
        }

        if (CollectionUtils.isEmpty(source.getBoundIds())) {
            return waybill;
        }
        var bounds = sourceRepository.list(source.getBoundIds());
        if (CollectionUtils.isEmpty(bounds)) {
            logger.error("Waybill bounds no found: {}", source.getBoundIds());
            return waybill;
        }

        bounds.forEach(bound -> {
            if (bound.isBalance()) {
                waybill.addBalances(BalanceAssembler.assemble(bound.decode()));
            } else if (bound.isQuotation()) {
                waybill.addQuotations(QuotationAssembler.assemble(bound.decode()));
            } else if (bound.isBill()) {
                waybill.addBills(BillAssembler.assemble(bound.decode()));
            }
        });

        return waybill;
    }

    public List<Combine> getCombineByIds(List<CombineId> combineIds) {
        if (CollectionUtils.isEmpty(combineIds)) {
            return Collections.emptyList();
        }

        return combineIds.stream().map(this::getCombineById).collect(Collectors.toList());
    }

    public Combine getCombineById(CombineId combineId) {
        var source = sourceRepository.ofId(new SourceId(combineId));
        if (null == source) {
            logger.info("加载合单索引数据: {}", combineId);
            source = this.create(combineId);
        }

        var combine = CombineAssembler.assemble(source.decode());
        if (null == combine) {
            logger.error("Combine source illegal: {}", source);
            return null;
        }

        if (CollectionUtils.isEmpty(source.getBoundIds())) {
            return combine;
        }

        var bounds = sourceRepository.list(source.getBoundIds());
        if (CollectionUtils.isEmpty(bounds)) {
            logger.error("Combine bounds no found: {}", source.getBoundIds());
            return combine;
        }

        bounds.forEach(bound -> {
            if (bound.isQuotation()) {
                combine.addQuotations(QuotationAssembler.assemble(bound.decode()));
            }
        });

        return combine;
    }
}
